<?php //strip
/**
 * The simplest implementation of odWpPlugin_Model_Interface.
 * 
 * @package odWpPlugin
 * @author Ondrej Donek <ondrejd@gmail.com>
 * 
 * @todo Finish odWpPlugin_Model_Simple::update($aFilter = '', $aValues = array())
 * @todo Finish odWpPlugin_Model_Simple::remove($aFilter = '')
 * @todo Finish odWpPlugin_Model_Simple::_checkValues($aValues = array())
 * @todo The property odWpPlugin_Model_Simple::$mCheckColumn is badly designed.
 *       Much better will be array with validators for all columns.
 */
class odWpPlugin_Model_Simple implements odWpPlugin_Model_Interface
{
  /**
   * Holds default column type.
   * 
   * @var string
   */
  const DEFAULT_COLUMN_TYPE = 'char';
  
  /** 
   * Name of database table. 
   * 
   * @var string
   */
  protected $mTableName = null;
  
  /** 
   * Holds description of single database table columns.
   * 
   * @var array
   */
  protected $mColumns = array();
  
  /** 
   * Name of primary column.
   * 
   * @var string 
   */
  protected $mPrimary = null;
  
  /** 
   * Name of column used for checking given values.
   * 
   * @var string 
   */
  protected $mCheckColumn = null;
  
  /** 
   * Array with class names of models of dependant tables.
   * 
   * @var array 
   */
  protected $mDependentTables = array();
  
  /**
  * Constructor - sets up model according to given parameters.
  * 
  * Example parameters:
  * <code>
  * <?php
  * $params = array(
  *   'tableName' => 'sample_table',
  *   'primaryColumn' => 'ID',
  *   'checkColumn' => 'title',
  *   'colums' => array(
  *     'ID' => array('type' => 'bigint', 'length' => 20, 'primary' => true),
  *     'title' => array('type' => 'char', 'length' => 255, 'notnull' => true, 'default' => ''),
  *     'description' => array('type' => 'text')
  *   )
  * );
  * ?>
  * </code>
  * 
  * @param array $aParams
  * @return void
  */
  public function __construct($aParams = null)
  {
    if(!is_array($aParams)) 
      throw new Exception('odWpPlugin_Model_Simple should be  ' . 
        'initialized with array of parameters!');
    
    if(!array_key_exists('tableName', $aParams) && 
       !array_key_exists('primaryColumn', $aParams) && 
       !array_key_exists('columns', $aParams)) 
      throw new Exception('Some required parameters missing! Required ' . 
        'parameters are `tableName`, `primaryColumn` ' . 
        'and `columns`.');
    
    if(!is_array($aParams['columns'])) 
      throw new Exception('Given columns are not an array!');
    
    if(count($aParams['columns']) == 0) 
      throw new Exception('No columns given!');
    
    $this->mTableName = $aParams['tableName'];
    $this->mPrimary = $aParams['primaryColumn'];
    $this->mColumns = $aParams['columns'];
    
    $this->mCheckColumn = (array_key_exists('checkColumn', $aParams))
  ? $aParams['checkColumn']
  : $aParams['primaryColumn'];
  }
  
  /**
   * Returns name of the model.
   * 
   * @return string
   */
  public function getTableName() 
  {
    return $this->mTableName;
  }
  
  /**
   * Returns primary column of the model.
   * 
   * @return string
   */
  public function getPrimaryColumn()
  {
    return $this->mPrimary;
  }
  
  /**
   * Returns check column of the model.
   * 
   * If check column is not specified in the parameters given to the model's 
   * constructor will be used primary column. The check column is used when 
   * testing if the form created alongside the model is submitted or not.
   * 
   * @return string
   */
  public function getCheckColumn()
  {
    return $this->mCheckColumn;
  }
  
  /**
   * Creates table if not exists for described database model.
   *
   * @global wpdb $wpdb
   * @return boolean
   */
  public function createTable()
  {
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    // Firstly normalize column options
    // TODO Normalize columns should be better to be executed in the constructor.
    $this->_normalizeColumns();
    
    // Create CREATE TABLE query
    $query = 'CREATE TABLE IF NOT EXISTS `' . $this->mTableName . '` (' . PHP_EOL;
    $primaryCol = null;
    
    foreach($this->mColumns as $colName => $colVal) {
      $query .= '`' . $colName . '` ' . $colVal['type'] . ' ';
      
      if(!is_null($colVal['length'])) $query .= '( ' . $colVal['length'] . ' ) ';
      if($colVal['notnull'] === true) $query .= 'NOT NULL ';
      
      if(!is_null($colVal['default'])) {
  if($colVal['type'] == 'timestamp' && $colVal['default'] == 'CURRENT_TIMESTAMP') {
    $query .= 'DEFAULT CURRENT_TIMESTAMP ';
  } else {
    $query .= 'DEFAULT \'' . $colVal['default'] . '\' ';
  }
      }
      
      if($colVal['autoincrement'] === true) $query .= 'AUTO_INCREMENT ';
      if($colVal['primary'] === true) $primaryCol = $colName;
      if(!is_null($colVal['comment'])) $query .= 'COMMENT \'' . $colVal['comment'] . '\' ';
      
      $query .= ', ' . PHP_EOL;
    }
    
    if(!is_null($primaryCol)) {
      $query .= 'PRIMARY KEY ( `' . $primaryCol . '` ) ' . PHP_EOL;
    } else {
      $query = str_replace(', ' . PHP_EOL . ') ENGINE', PHP_EOL . ') ENGINE', $query);
    }
    
    $query .= ') ENGINE = MYISAM CHARACTER SET utf8 COLLATE utf8_general_ci; ';
    $wpdb->query($query);
    
    return true;
  }
  
  /**
   * Drops table from the database if already exists.
   * 
   * @return boolean
   */
  public function dropTable()
  {
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    $wpdb->query('DROP TABLE IF EXISTS `' . $this->mTableName . '`; ');
    
    return true;
  }
  
  /**
   * Returns TRUE if column with given name exists in the model.
   *
   * @param string $aColName
   * @return boolean
   */
  public function hasColumn($aColName = '')
  {
    return array_key_exists($aColName, $this->mColumns);
  }
  
  /**
   * Returns array with description of the column with given name.
   * If column wasn't found NULL is returned.
   *
   * @param string $aColName
   * @return array|null
   */
  public function getColumn($aColName = '')
  {
    if(!$this->hasColumn($aColName)) {
      return null;
    }
    
    return $this->mColumns[$aColName];
  }
  
  /**
   * Returns array with columns descriptions that matches given filter. 
   * If filter is null are returned all columns otherwise can filter be
   * defined as a array with column names or string with column names
   * separated by commas.
   *
   * @param array|string|null $aColName
   * @return array
   */
  public function getColumns($aFilter = null)
  {
    if(is_null($aFilter)) {
      return $this->mColumns;
    }
    
    if(is_string($aFilter)) {
      $aFilter = explode(',', $aFilter);
    }
    
    $ret = array();
    if(is_array($aFilter)) {
      foreach($aFilter as $colName) {
  if($this->hasColumn($colName)) {
    $ret[$colName] = $this->mColumns[$colName];
  }
      }
    }
    
    return $ret;
  }
  
  /**
   * Insert new item(s) from the given values.
   *
   * @global wpdb $wpdb
   * @param array $aValues
   * @param boolean $aMoreRows
   * @return boolean
   * 
   * @todo Validate given values!!!
   */
  public function insert($aValues = array(), $aMoreRows = false)
  {
    if(!is_array($aValues)) {
      throw new Exception('First argument expcected to be an array!');
    }
    
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    $query = 'INSERT INTO `' . $this->mTableName . '` ( ';
    $cols = $vals = '';
    
    if($aMoreRows !== true) {
      // We are inserting single item
      foreach($aValues as $key => $val) {
        $cols .= ((!empty($cols)) ? ', ' : '') . '`' . $key . '`';
        // TODO Escape single quotes!!!
        $vals .= (($vals != '') ? ', ' : '') . "'$val'";
      }
      
      $query .= $cols . ' ) ' . PHP_EOL . 'VALUES ( ' . $vals . ' );';
    } else {
      // We are inserting more then one item
      foreach($aValues[0] as $key => $val) {
        $cols .= (($cols != '') ? ', ' : '') . '`' . $key . '`';
      }
      
      $query .= $cols . ' ) ' . PHP_EOL . 'VALUES ' . PHP_EOL;
      
      foreach($aValues as $values) {
        $row = '';
        foreach($values as $key => $val) {
          // TODO Escape single quotes!!!
          $row .= ((!empty($row)) ? ', ' : '') . "'$val'";
        }
        $query .= '( ' . $row . ' ), ';
      }
      
      $query = rtrim($query, ' ');
      $query = rtrim($query, ',');
      
      $query .= ';';
    }
    
    $res = $wpdb->query($query);
    return ($res) ? true : false;
  }
  
  /**
   * Returns count of items which matches given filter.
   *
   * @global wpdb $wpdb
   * @param string $aFilter
   * @return number
   */
  public function getCount($aFilter = '')
  {
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    $query = 'SELECT count(`' . $this->mPrimary . '`) AS `itemsCount` ' .
       'FROM `' . $this->mTableName . '` AS `t1` ' .
       'WHERE ' . $aFilter;
    $res = $wpdb->get_results($query);
    
    return (int) $res[0]->itemsCount;
  }
  
  /**
   * Permanetly removes items which matches the filter from database. 
   * If only one matches the filter is returned its ID otherwise is returned
   * count of removed items.
   *
   * @global wpdb $wpdb
   * @param string $aFilter WHERE part of query (e.g. '`t1`.`ID` = 1')
   * @return integer|false
   */
  public function remove($aFilter = '')
  {
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    // Ensure that there are no `t1` in the $aFilter
    $aFilter = str_replace('`t1`.', '', $aFilter);
    
    $query = 'DELETE FROM `' . $this->mTableName . '` WHERE ' . $aFilter;
    $res = $wpdb->query($query);
    
    return $wpdb->rows_affected;
  }
  
  /**
   * Returns items which matches given filter.
   * 
   * @global wpdb $wpdb
   * @param string $aFilter WHERE part of query (e.g. '`t1`.`ID` = 1')
   * @param number $aCount
   * @param number $aLimitFrom
   * @param string $aOrderBy ORDER BY part of query (e.g. '`t1`.`ID` DESC')
   * @return array
   * 
   * @todo `$aOrderBy` should be the second!!!
   */
  public function select($aFilter = '', $aCount = 0, $aLimitFrom = 0, $aOrderBy = '')
  {
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    $query = 'SELECT `t1`.* FROM `' . $this->mTableName . '` AS `t1` WHERE ' . $aFilter;
    
    if($aLimitFrom > 0 && $aCount > 0) {
      $query .= 'LIMIT ' . $aLimitFrom . ', ' . $aCount . ' ';
    }
    else if($aCount > 0) {
      $query .= 'LIMIT ' . $aCount . ' ';
    }
    
    if(!empty($aOrderBy)) {
      $query .= 'ORDER BY ' . $aOrderBy;
    }
    
    $query .= ';';
    
    return $wpdb->get_results($query, ARRAY_A);
  }
  
  /**
   * Updates items which matches filter with the given values.
   *
   * @global wpdb $wpdb
   * @param string $aFilter WHERE part of query (e.g. '`t1`.`ID` = 1')
   * @param array $aValues
   * @return void
   * 
   * @todo Validate given values!!!
   * @todo Add limit!!!
   */
  public function update($aFilter = '', $aValues = array())
  {
    if(!is_array($aValues)) {
      throw new Exception('The second argument expcected to be an array!');
    }
    
    if(defined('IS_UNITTESTING') && IS_UNITTESTING) {
      $wpdb = new wpdb(DB_USER, DB_PASSWORD, DB_NAME, DB_HOST);
    } else {
      global $wpdb;
    }
    
    // Ensure that there are no `t1` in the $aFilter
    $aFilter = str_replace('`t1`.', '', $aFilter);
    
    $query = 'UPDATE `' . $this->mTableName . '` SET ';
    $vals = '';

    foreach($aValues as $key => $val) {
      $vals .= (empty($vals) ? '' : ', ') . "`{$key}` = '{$val}' ";
    }

    $query .= $vals . ' WHERE ' . $aFilter . ' ;';

    $wpdb->query($query);
    
    return $wpdb->rows_affected;
  }

  /**
   * Check given array of values - it can be single or more rows.
   *
   * @param array $aValues
   * @return boolean
   * 
   * @todo Test given array if is correct or not!
   */
  protected function _checkValues($aValues = array())
  {
    throw new Exception('Not implemented yet!');
  }
  
  /**
   * Returns default length for the given column type.
   *
   * @param string $aType
   * @return string
   */
  protected function _getDefaultLengthForType($aType = self::DEFAULT_COLUMN_TYPE)
  {
    switch($aType) {
      case 'char'     : return '255';
      case 'int'      : return '11';
      case 'shortint' : return '1';
      case 'bigint'   : return '20';
      default         : return null;
    }
  }
  
  /**
   * Normalize table columns.
   *
   * @return void
   */
  protected function _normalizeColumns()
  {
    foreach($this->mColumns as $colName => $colOpts) {
      if(!array_key_exists('type', $colOpts)) {
        $this->mColumns[$colName]['type'] = self::DEFAULT_COLUMN_TYPE;
      }
      
      if(!array_key_exists('primary', $colOpts)) {
        $this->mColumns[$colName]['primary'] = false;
      }
      
      // Pokud je nastavena `primary` a typ obsahuje `int` pak predpokladame,
      // ze `autoincrement` je TRUE.
      if(!array_key_exists('autoincrement', $colOpts)) {
        $this->mColumns[$colName]['autoincrement'] =
            ($this->mColumns[$colName]['primary'] === true) ? true : false;
      }
      
      if(!array_key_exists('length', $colOpts)) {
        $this->mColumns[$colName]['length'] = $this->_getDefaultLengthForType($this->mColumns[$colName]['type']);
      }
      
      if(!array_key_exists('notnull', $colOpts)) {
        $this->mColumns[$colName]['notnull'] = false;
      }
      
      if(!array_key_exists('default', $colOpts)) {
        $this->mColumns[$colName]['default'] = null;
      }
      
      if(!array_key_exists('comment', $colOpts)) {
        $this->mColumns[$colName]['comment'] = null;
      }
    }
  }
  
} // End of odWpPlugin_Model_Simple
